package com.tank;

import com.almasb.fxgl.app.CursorInfo;
import com.almasb.fxgl.app.GameApplication;
import com.almasb.fxgl.app.GameSettings;
import com.almasb.fxgl.app.scene.FXGLMenu;
import com.almasb.fxgl.app.scene.GameView;
import com.almasb.fxgl.app.scene.SceneFactory;
import com.almasb.fxgl.core.math.FXGLMath;
import com.almasb.fxgl.core.util.LazyValue;
import com.almasb.fxgl.dsl.FXGL;
import com.almasb.fxgl.entity.Entity;
import com.almasb.fxgl.logging.Logger;
import com.almasb.fxgl.physics.CollisionHandler;
import com.almasb.fxgl.time.TimerAction;
import com.tank.collision.*;
import com.tank.components.TankComponent;
import com.tank.constant.Config;
import com.tank.constant.Direction;
import com.tank.constant.GameType;
import com.tank.factory.ItemEntityFactory;
import com.tank.factory.MapEntityFactory;
import com.tank.factory.TankEntityFactory;
import com.tank.ui.FailedScene;
import com.tank.ui.InfoPane;
import com.tank.ui.SucceedScene;
import com.tank.ui.TankMainMenu;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.input.KeyCode;
import javafx.scene.paint.Color;
import javafx.util.Duration;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

public class TankApp extends GameApplication {

    private static final Logger LOGGER = Logger.get("TankApp");

    private Entity player;

    private TimerAction reinforceTimer;

    private TimerAction enemySpawnTimer;

    private final LazyValue<FailedScene> failedSceneLazyValue = new LazyValue<>(FailedScene::new);

    private final LazyValue<SucceedScene> succeedSceneLazyValue = new LazyValue<>(SucceedScene::new);

    private void expireTimerAction(TimerAction freezeEnemyTimer) {
        if (null != freezeEnemyTimer && !freezeEnemyTimer.isExpired()) {
            freezeEnemyTimer.expire();
        }
    }

    public void reinforce() {
        expireTimerAction(reinforceTimer);
        Entity base = FXGL.getGameWorld().getEntitiesByType(GameType.BASE).get(0);
        Point2D basePosition = base.getPosition();
        List<Point2D> baseWallPositions = new ArrayList<>(8);
        baseWallPositions.add(new Point2D(basePosition.getX() - Config.CELL_SIZE, basePosition.getY() + Config.CELL_SIZE));
        baseWallPositions.add(new Point2D(basePosition.getX() - Config.CELL_SIZE, basePosition.getY()));
        baseWallPositions.add(new Point2D(basePosition.getX() - Config.CELL_SIZE, basePosition.getY() - Config.CELL_SIZE));
        baseWallPositions.add(new Point2D(basePosition.getX(), basePosition.getY() - Config.CELL_SIZE));
        baseWallPositions.add(new Point2D(basePosition.getX() + Config.CELL_SIZE, basePosition.getY() - Config.CELL_SIZE));
        baseWallPositions.add(new Point2D(basePosition.getX() + Config.CELL_SIZE * 2, basePosition.getY() - Config.CELL_SIZE));
        baseWallPositions.add(new Point2D(basePosition.getX() + Config.CELL_SIZE * 2, basePosition.getY()));
        baseWallPositions.add(new Point2D(basePosition.getX() + Config.CELL_SIZE * 2, basePosition.getY() + Config.CELL_SIZE));
        for (Point2D point : baseWallPositions) {
            List<Entity> entities = FXGL.getGameWorld().getEntitiesAt(point);
            for (Entity entity : entities) {
                entity.removeFromWorld();
            }
            FXGL.spawn("stone", point);
        }
        reinforceTimer = FXGL.runOnce(() -> {
            for (Point2D point : baseWallPositions) {
                List<Entity> entities = FXGL.getGameWorld().getEntitiesAt(point);
                for (Entity entity : entities) {
                    entity.removeFromWorld();
                }
                FXGL.spawn("brick", point);
            }
        }, Config.REINFORCE_TIME);
    }

    public void destroyBaseWall() {
        Entity base = FXGL.getGameWorld().getEntitiesByType(GameType.BASE).get(0);
        Point2D basePosition = base.getPosition();
        List<Point2D> baseWallPositions = new ArrayList<>(8);
        baseWallPositions.add(new Point2D(basePosition.getX() - Config.CELL_SIZE, basePosition.getY() + Config.CELL_SIZE));
        baseWallPositions.add(new Point2D(basePosition.getX() - Config.CELL_SIZE, basePosition.getY()));
        baseWallPositions.add(new Point2D(basePosition.getX() - Config.CELL_SIZE, basePosition.getY() - Config.CELL_SIZE));
        baseWallPositions.add(new Point2D(basePosition.getX(), basePosition.getY() - Config.CELL_SIZE));
        baseWallPositions.add(new Point2D(basePosition.getX() + Config.CELL_SIZE, basePosition.getY() - Config.CELL_SIZE));
        baseWallPositions.add(new Point2D(basePosition.getX() + Config.CELL_SIZE * 2, basePosition.getY() - Config.CELL_SIZE));
        baseWallPositions.add(new Point2D(basePosition.getX() + Config.CELL_SIZE * 2, basePosition.getY()));
        baseWallPositions.add(new Point2D(basePosition.getX() + Config.CELL_SIZE * 2, basePosition.getY() + Config.CELL_SIZE));
        for (Point2D point : baseWallPositions) {
            List<Entity> entities = FXGL.getGameWorld().getEntitiesAt(point);
            for (Entity entity : entities) {
                entity.removeFromWorld();
            }
        }
    }

    public void fail() {
        FXGL.getSceneService().pushSubScene(failedSceneLazyValue.get());
    }

    public void startLevel() {
        FXGL.play("start.wav");
        //init game variants
        FXGL.set(Config.ENEMY_AMOUNT, 0);
        FXGL.set(Config.ENEMY_DESTROY_AMOUNT, 0);
        expireTimerAction(enemySpawnTimer);
        expireTimerAction(reinforceTimer);
        //init map
        int mapLevel = FXGL.geti(Config.MAP_LEVEL);
        LOGGER.info("startLevel: " + mapLevel + "...");
        FXGL.setLevelFromMap("level" + mapLevel + ".tmx");
        //init player tanks
        List<Entity> playerSpawnPositions = FXGL.getGameWorld().getEntitiesByType(GameType.PLAYER_SPAWN);
        player = FXGL.spawn("player", playerSpawnPositions.get(0).getPosition());
        //init info board
        GameView gameView = new GameView(new InfoPane(), 1);
        FXGL.getGameScene().addGameView(gameView);
        //init enemy tanks
        List<Entity> enemySpawnPositions = FXGL.getGameWorld().getEntitiesByType(GameType.ENEMY_SPAWN);
        for (Entity enemySpawnPosition : enemySpawnPositions) {
            FXGL.spawn("enemy", enemySpawnPosition.getPosition());
            FXGL.inc(Config.ENEMY_AMOUNT, 1);
        }
        enemySpawnTimer = FXGL.run(() -> {
            int enemyAmount = FXGL.geti(Config.ENEMY_AMOUNT);
            if (enemyAmount >= Config.MAX_ENEMY_AMOUNT) {
                expireTimerAction(enemySpawnTimer);
                return;
            }
            Optional<Entity> randomOptional = FXGLMath.random(enemySpawnPositions);
            Point2D enemySpawnPosition;
            if (randomOptional.isPresent()) {
                enemySpawnPosition = randomOptional.get().getPosition();
            } else {
                throw new IllegalStateException("enemySpawnPositions为空");
            }
            List<Entity> entities = FXGL.getGameWorld().getEntitiesInRange(new Rectangle2D(enemySpawnPosition.getX(), enemySpawnPosition.getY(), Config.TANK_WIDTH, Config.TANK_HEIGHT));
            entities = entities.stream().filter(entity -> entity.isType(GameType.STONE) || entity.isType(GameType.SEA) || entity.isType(GameType.PLAYER) || entity.isType(GameType.ENEMY)).collect(Collectors.toList());
            if (entities.isEmpty()) {
                FXGL.spawn("enemy", enemySpawnPosition);
                FXGL.inc(Config.ENEMY_AMOUNT, 1);
            }
        }, Duration.seconds(0.5));
    }

    @Override
    protected void initSettings(GameSettings gameSettings) {
        gameSettings.setTitle("Tank");
        gameSettings.setVersion("0.1");
        gameSettings.setHeight(Config.GROUND_CELL_COUNT * Config.CELL_SIZE);
        gameSettings.setWidth((Config.GROUND_CELL_COUNT + Config.BOARD_CELL_COUNT) * Config.CELL_SIZE);
        gameSettings.setAppIcon("icon.png");
        gameSettings.setDefaultCursor(new CursorInfo("cursor.png", 0, 0));
        gameSettings.setMainMenuEnabled(true);
        gameSettings.getCSSList().add("main_menu.css");
        gameSettings.setSceneFactory(new SceneFactory() {
            @NotNull
            @Override
            public FXGLMenu newMainMenu() {
                return new TankMainMenu();
            }
        });
    }

    @Override
    protected void initGameVars(Map<String, Object> vars) {
        vars.put(Config.ENEMY_AMOUNT, 0);
        vars.put(Config.ENEMY_DESTROY_AMOUNT, 0);
        vars.put(Config.MAP_LEVEL, 0);
    }

    @Override
    protected void initGame() {
        FXGL.getGameScene().setBackgroundColor(Color.BLACK);
        FXGL.getGameWorld().addEntityFactory(new TankEntityFactory());
        FXGL.getGameWorld().addEntityFactory(new MapEntityFactory());
        FXGL.getGameWorld().addEntityFactory(new ItemEntityFactory());
        startLevel();
        //listen to enemy destroy amount
        FXGL.getip(Config.ENEMY_DESTROY_AMOUNT).addListener((observable, oldValue, newValue) -> {
            if (newValue.intValue() == Config.MAX_ENEMY_AMOUNT) {
                List<Entity> bullets = FXGL.getGameWorld().getEntitiesByType(GameType.BULLET);
                for (Entity bullet : bullets) {
                    bullet.removeFromWorld();
                }
                FXGL.runOnce(() -> FXGL.getSceneService().pushSubScene(succeedSceneLazyValue.get()), Duration.seconds(1));
            }
        });
    }

    @Override
    protected void initPhysics() {
        FXGL.getPhysicsWorld().addCollisionHandler(new BulletEnemyCollideHandler());
        FXGL.getPhysicsWorld().addCollisionHandler(new BulletPlayerCollideHandler());
        FXGL.getPhysicsWorld().addCollisionHandler(new BulletBorderCollideHandler());
        FXGL.getPhysicsWorld().addCollisionHandler(new BulletBulletCollideHandler());
        CollisionHandler bulletBrickCollisionHandler = new BulletTerrainCollideHandler(GameType.BULLET, GameType.BRICK);
        FXGL.getPhysicsWorld().addCollisionHandler(bulletBrickCollisionHandler);
        FXGL.getPhysicsWorld().addCollisionHandler(bulletBrickCollisionHandler.copyFor(GameType.BULLET, GameType.STONE));
        FXGL.getPhysicsWorld().addCollisionHandler(bulletBrickCollisionHandler.copyFor(GameType.BULLET, GameType.GREENS));
        FXGL.getPhysicsWorld().addCollisionHandler(new BulletBaseCollideHandler());
        FXGL.getPhysicsWorld().addCollisionHandler(new ItemPlayerCollideHandler());
        FXGL.getPhysicsWorld().addCollisionHandler(new ItemEnemyCollideHandler());
    }

    private boolean isPlayerActive() {
        return null != player && player.isActive();
    }

    @Override
    protected void initInput() {
        FXGL.onKey(KeyCode.W, () -> playerMove(Direction.UP));
        FXGL.onKey(KeyCode.S, () -> playerMove(Direction.DOWN));
        FXGL.onKey(KeyCode.A, () -> playerMove(Direction.LEFT));
        FXGL.onKey(KeyCode.D, () -> playerMove(Direction.RIGHT));
        FXGL.onKey(KeyCode.SPACE, () -> {
            if (isPlayerActive()) {
                TankComponent component = player.getComponent(TankComponent.class);
                component.shoot();
            }
        });
    }

    private void playerMove(Direction up) {
        if (isPlayerActive()) {
            TankComponent component = player.getComponent(TankComponent.class);
            component.moveForDirection(up);
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}
